const entityMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
    '/': '&#x2F;',
    '`': '&#x60;',
    '=': '&#x3D',
}

function escapeHtml(string) {
    return String(string).replace(/[&<>"'`=\/]/g, s => entityMap[s])
}

function escapeRegExp(str) {
    return str.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
}

function parseTemplate(template) {
    const tokens = []
    const scanner = new Scanner(template);
    let openingTagRe;
    let closingTagRe;
    let closingCurlyRe;
    function compileTags(tagsToCompile) {
        if (typeof tagsToCompile === 'string') {
            tagsToCompile = tagsToCompile.split(/\s+/, 2);
        }
        if (Array.isArray(tagsToCompile) && tagsToCompile.length === 2) {
            openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
            closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
            closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
        }
    }
    compileTags('{{ }}');

    let spaces = []
    let hasTag = false
    let nonSpace = false
    let value
    // 类型
    let type
    let start
    let indentation = ''
    let tagIndex = 0
    let lineHasNonSpace = false
    // 非源代码内容，用于防止while死循环
    let temp = 0

    function stripSpace() {
        if (hasTag && !nonSpace) {
            while (spaces.length) {
                delete tokens[spaces.pop()]
            }
        } else {
            spaces = []
        }
        hasTag = false
        nonSpace = false
    }

    while (temp < 1000 && !scanner.eos()) {
        temp++
        start = scanner.pos;
        // 找到开始标签之前的文本内容
        value = scanner.scanUntil(openingTagRe)
        if (value) {
            for (const chr of value) {
                if (/\s/.test(chr)) {
                    spaces.push(tokens.length)
                    indentation += chr
                } else {
                    nonSpace = true
                    indentation = ''
                    lineHasNonSpace = true
                }
                tokens.push(['text', chr, start, ++start])
                if (chr === '\n') {
                    // 清除空格
                    stripSpace()
                    indentation = ''
                    tagIndex = 0
                    lineHasNonSpace = false
                }
            }
        }
        if (!scanner.scan(openingTagRe)) {
            // 说明匹配结束了
            break;
        }
        // 有开始标签，则表示有tag
        hasTag = true
        type = scanner.scan(/\{|#|\/|\^|!|\>|=/) || 'name'
        scanner.scan(/\s*/)
        if (type === '=') {
            value = scanner.scanUntil(/\s*=/)
            scanner.scan(/\s*=/)
            scanner.scanUntil(closingTagRe)
        } else if (type === '{') {
            value = scanner.scanUntil(closingCurlyRe)
            scanner.scan(/\s*\}/)
            // 后面会统一处理，所以先移动到闭合标签之前
            scanner.scanUntil(closingTagRe)
            type = '&'
        } else {
            // 程序执行到这里，说明有开始标签，那需要找一下开始标签到结束标签的内容
            value = scanner.scanUntil(closingTagRe)
        }
        scanner.scan(closingTagRe)
        // 找到结束标签之前的文本内容
        if (type === '>') {
            tokens.push([type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace])
        } else {
            tokens.push([type, value, start, scanner.pos])
        }
        tagIndex++
        if (type === '=') {
            compileTags(value)
        }
    }

    stripSpace();

    return nestTokens(squashTokens(tokens))
}

function squashTokens(tokens) {
    const squashedTokens = []
    let lastToken
    for (const token of tokens) {
        if (token) {
            if (token[0] === 'text') {
                if (lastToken) {
                    lastToken[1] += token[1]
                    lastToken[3] = token[3]
                } else {
                    lastToken = token
                    squashedTokens.push(lastToken)
                }
            } else {
                squashedTokens.push(token)
                lastToken = undefined
            }
        }
    }
    return squashedTokens
}

function nestTokens(tokens) {
    const nestedTokens = [];
    let collector = nestedTokens
    const sections = []
    let section
    for (const token of tokens) {
        switch (token[0]) {
            case '#':
            case '^':
                collector.push(token)
                sections.push(token)
                collector = token[4] = []
                break;
            case '/':
                section = sections.pop()
                section[5] = token[2]
                collector = sections.length ? sections[sections.length - 1][4] : nestedTokens
                break;
            default:
                collector.push(token)

        }
    }
    return nestedTokens
}

class Context {
    constructor(view, parentContext) {
        this.view = view;
        this.parent = parentContext;
    }
    push(view) {
        return new Context(view, this)
    }
    lookup(name) {
        let context = this;
        let value = context.view[name]
        if (typeof value === 'function') {
            value = value.call(this.view)
        }
        return value
    }
}

class Scanner {
    constructor(string) {
        this.string = string;
        this.tail = string;
        this.pos = 0
    }
    // 判断是否已经结束了
    eos() {
        return this.tail === ''
    }
    scan(re) {
        const match = this.tail.match(re);
        if (match && match.index === 0) {
            // 有匹配并且必须要开头就匹配
            const value = match[0]
            this.tail = this.tail.substring(value.length);
            this.pos += value.length
            return value
        }
        return ''
    }
    scanUntil(re) {
        const index = this.tail.search(re)
        let match
        if (index <= -1) {
            // 无匹配结果
            match = this.tail
            this.tail = ''
            this.pos += match.length
        } else if (index === 0) {
            // 起始就匹配
            match = ''
        } else {
            match = this.tail.substring(0, index)
            this.tail = this.tail.substring(index)
            this.pos += index
        }
        return match
    }
}


class Writer {
    render(template, view, partials) {
        const tokens = this.parse(template)
        const context = new Context(view);
        // 解析完成，进入下一步处理
        return this.renderTokens(tokens, context, partials, template);
    }
    parse(template) {
        // 这里将来要做一下缓存
        return parseTemplate(template)
    }
    renderTokens(tokens, context, partials, originalTemplate) {
        let buffer = ''
        let symbol
        let value
        const self = this
        const subRender = function (template) {
            return self.render(template, context.view)
        }
        for (const token of tokens) {
            symbol = token[0]
            switch (symbol) {
                case 'name':
                    value = context.lookup(token[1])
                    value =
                        buffer += this.escapedValue(value == null ? '' : value)
                    break
                case '&':
                    value = context.lookup(token[1])
                    buffer += value == null ? '' : value;
                    break
                case 'text':
                    buffer += token[1]
                    break
                case '#':
                    value = context.lookup(token[1])
                    if (!value) {
                        return
                    }
                    if (Array.isArray(value)) {
                        for (const item of value) {
                            buffer += this.renderTokens(token[4], context.push(item), partials)
                        }
                    } else if (typeof value === 'object') {
                        buffer += this.renderTokens(token[4], context.push(value), partials)
                    } else if (typeof value === 'function') {
                        value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender)
                        if (value != null) {
                            buffer += value
                        }
                    } else {
                        buffer += this.renderTokens(token[4], context, partials)
                    }
                    break
                case '^':
                    value = context.lookup(token[1])
                    if (!value || (Array.isArray(value) && value.length === 0)) {
                        buffer += this.renderTokens(token[4], context, partials)
                    }
                    break
                case '>':
                    buffer += this.renderPartial(token, context, partials)
                    break
            }
        }
        return buffer
    }
    escapedValue(text) {
        return escapeHtml(text)
    }
    indentPartial(partial, indendation, lineHasNonSpace) {
        const filteredIndentation = indendation.replace(/[^ \t]/g, '')
        let partialByNl = partial.split('\n')
        for (let i = 0; i < partialByNl.length; i++) {
            if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) {
                partialByNl[i] = filteredIndentation + partialByNl[i]
            }
        }
        return partialByNl.join('\n');
    }
    renderPartial(token, context, partials) {
        const value = partials[token[1]]

        if (value != null) {
            const lineHasNonSpace = token[6]
            const tagIndex = token[5]
            const indentation = token[4]
            let indentedValue = value
            if (tagIndex === 0 && indentation) {
                indentedValue = this.indentPartial(value, indentation, lineHasNonSpace)
            }
            const tokens = this.parse(indentedValue)
            return this.renderTokens(tokens, context, partials)
        }
    }
}

const defaultWriter = new Writer();

const mustache = {
    render(template, view, partials) {
        document.getElementById('template').textContent = template;
        const output = defaultWriter.render(template, view, partials)
        return output
    }
}

export default mustache;